// luaado.cpp : Defines the exported functions for the DLL application.
//

#include "stdafx.h"
#include "msado15.h"

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

#ifdef __cplusplus 
extern "C" {
#endif
#include "lua.h"
#include "lauxlib.h"
#if ! defined (LUA_VERSION_NUM) || LUA_VERSION_NUM < 501
#include "compat-5.1.h"
#endif

#ifdef __cplusplus 
}
#endif

#include "luasql.h"

#define LUASQL_ENVIRONMENT_ADODB "ADODB environment"
#define LUASQL_CONNECTION_ADODB "ADODB connection"
#define LUASQL_CURSOR_ADODB "ADODB cursor"

typedef struct {
	short      closed;
} env_data;


typedef struct {
	short			closed;
	int				env;                /* reference to environment */
	int				auto_commit;        /* 0 for manual commit */
	_Connection*    ado_conn;
} conn_data;


typedef struct {
	short      closed;
	int        conn;               /* reference to connection */
	int        numcols;            /* number of columns */
	int		   numrows;
	int        colnames, coltypes; /* reference to column information tables */
	int        curr_tuple;         /* next tuple to be read */
	_Recordset  *ado_res;
} cur_data;


typedef void (*creator) (lua_State *L, cur_data *cur);

static int create_cursor (lua_State *L, int rows, struct _Recordset* result);

LUAADODB_API int luaopen_luasql_adodb(lua_State *L);


/**
 * 
 **/
int luasql_faildirect_com_error( lua_State* L, _com_error& e)
{
	_bstr_t msg( e.ErrorMessage() );
	return luasql_faildirect( L, msg );
}

/*
** Check for valid environment.
*/
static env_data *getenvironment (lua_State *L) {
	env_data *env = (env_data *)luaL_checkudata (L, 1, LUASQL_ENVIRONMENT_ADODB);
	luaL_argcheck (L, env != NULL, 1, LUASQL_PREFIX"environment expected");
	luaL_argcheck (L, !env->closed, 1, LUASQL_PREFIX"environment is closed");
	return env;
}


/*
** Check for valid connection.
*/
static conn_data *getconnection (lua_State *L) {
	conn_data *conn = (conn_data *)luaL_checkudata (L, 1, LUASQL_CONNECTION_ADODB);
	luaL_argcheck (L, conn != NULL, 1, LUASQL_PREFIX"connection expected");
	luaL_argcheck (L, !conn->closed, 1, LUASQL_PREFIX"connection is closed");
	return conn;
}


/*
** Check for valid cursor.
*/
static cur_data *getcursor (lua_State *L) {
	cur_data *cur = (cur_data *)luaL_checkudata (L, 1, LUASQL_CURSOR_ADODB);
	luaL_argcheck (L, cur != NULL, 1, LUASQL_PREFIX"cursor expected");
	luaL_argcheck (L, !cur->closed, 1, LUASQL_PREFIX"cursor is closed");
	return cur;
}


/*
** Push the value of #i field of #tuple row.
*/
static void pushvalue (lua_State *L,_RecordsetPtr res, int tuple, int i) {
	
	_variant_t t(tuple + 1);
	FieldPtr field = res->Fields->GetItem( t );
	if( field->Status ){
		lua_pushnil (L);
	}
	else
	{
		_variant_t v = field->GetValue();
		_bstr_t    bstr = v;
		lua_pushstring (L, bstr);
	}
}

/*
** Push the value of #i field of #tuple row.
*/
static void pushsinglevalue (lua_State *L, Field* field) {
	
	//_variant_t v = field->GetValue();
	///_bstr_t    bstr = v;
	_variant_t v = field->GetValue();
	switch ( v.vt )
	{
	case VT_NULL:
	case VT_EMPTY:
		lua_pushnil(L);
		break;
	default:
		{
		_bstr_t bstr = v;
		lua_pushstring (L, bstr);
		}
		break;
	}
	
}


/*
** Get another row of the given cursor.
*/
static int cur_fetch (lua_State *L) {
	cur_data *cur = getcursor (L);
	_RecordsetPtr res = cur->ado_res;
	int tuple = cur->curr_tuple;

	if (res->adoEOF) {
		lua_pushnil(L);  /* no more results */
		return 1;
	}


	cur->curr_tuple++;
	if (lua_istable (L, 2)) {
		int i;
		const char *opts = luaL_optstring (L, 3, "n");
		if (strchr (opts, 'n') != NULL)
			/* Copy values to numerical indices */
			for (i = 1; i <= cur->numcols; i++) {
				pushvalue (L, res, tuple, i);
				lua_rawseti (L, 2, i);
			}
		if (strchr (opts, 'a') != NULL){
			FieldsPtr fields = res->GetFields();
			long fieldCount = 0;
			fields->get_Count( &fieldCount );
			for (i = 0; i<fieldCount; i++) {
				try{
					Field *field = NULL;
					VARIANT vt;
					vt.intVal = i;
					vt.vt = VT_I4;
					fields->get_Item( vt, &field );
					lua_pushstring (L, field->GetName());
					pushsinglevalue (L, field);
					lua_rawset (L, 2);
				}catch( _com_error e)
				{
					printf( "Error: %s\n", (char*)_bstr_t(e.ErrorMessage()) );
				}
			}
		}
		
		lua_pushvalue(L, 2);
		res->MoveNext();
		return 1; /* return table */
	}
	else {
		int i;
		luaL_checkstack (L, cur->numcols, LUASQL_PREFIX"too many columns");
		for (i = 1; i <= cur->numcols; i++)
			pushvalue (L, res, tuple, i);
		return cur->numcols; /* return #numcols values */
	}
}


/*
** Close the cursor on top of the stack.
** Return 1
*/
static int cur_close (lua_State *L) {
	cur_data *cur = (cur_data *)luaL_checkudata (L, 1, LUASQL_CURSOR_ADODB);
	luaL_argcheck (L, cur != NULL, 1, LUASQL_PREFIX"cursor expected");
	try{
		cur->ado_res->Close();
	}catch( _com_error e)
	{
		//do nothing
	}
	if (cur->closed) {
		lua_pushboolean (L, 0);
		return 1;
	}

	/* Nullify structure fields. */
	cur->closed = 1;
	//PQclear(cur->pg_res);
	luaL_unref (L, LUA_REGISTRYINDEX, cur->conn);
	luaL_unref (L, LUA_REGISTRYINDEX, cur->colnames);
	luaL_unref (L, LUA_REGISTRYINDEX, cur->coltypes);

	lua_pushboolean (L, 1);
	return 1;
}


/*
** Get the internal database type of the given column.
*/
static char *getcolumntype (Field* field, char *buff, size_t lenbuf) {
	DataTypeEnum dt = field->GetType();
	long definesize = field->DefinedSize;
	switch( dt )
	{
	case  adArray:
		sprintf_s( buff, lenbuf, "array(%d)", definesize );
		break;
	case  adBigInt:
		sprintf_s( buff, lenbuf, "bigint");
		break;
	case  adBinary:
		sprintf_s( buff, lenbuf, "blob");
		break;
	case  adBoolean:
		sprintf_s( buff, lenbuf, "boolean");
		break;
	case  adBSTR:
		sprintf_s( buff, lenbuf, "bstr(%d)", definesize);
		break;
	case  adChapter:
		sprintf_s( buff, lenbuf, "chapter(%d)", definesize);
		break;
	case  adChar:
		sprintf_s( buff, lenbuf, "char(%d)", definesize);
		break;
	case  adCurrency:
		sprintf_s( buff, lenbuf, "currency");
		break;
	case  adDate:
		sprintf_s( buff, lenbuf, "date");
		break;

	case  adDBDate:
		sprintf_s( buff, lenbuf, "date");
		break;

	case  adDBTime:
		sprintf_s( buff, lenbuf, "time");
		break;

	case  adDBTimeStamp:
		sprintf_s( buff, lenbuf, "timestamp");
		break;
		
	case  adDecimal:
		sprintf_s( buff, lenbuf, "decimal(%d, %d)", definesize, field->Precision);
		break;

	case  adDouble:
		sprintf_s( buff, lenbuf, "double(%d)", definesize, field->Precision);
		break;

	case  adEmpty:
		sprintf_s( buff, lenbuf, "empty");
		break;

	case  adError:
		sprintf_s( buff, lenbuf, "error");
		break;

	case  adFileTime:
		sprintf_s( buff, lenbuf, "filetime");
		break;

	case  adGUID:
		sprintf_s( buff, lenbuf, "GUID");
		break;

	case  adIDispatch:
		sprintf_s( buff, lenbuf, "ole");
		break;

	case  adInteger:
		sprintf_s( buff, lenbuf, "int");
		break;

	case  adIUnknown:
		sprintf_s( buff, lenbuf, "ole(unkown)");
		break;

	case  adLongVarBinary:
		sprintf_s( buff, lenbuf, "long binary");
		break;

	case  adLongVarChar:
		sprintf_s( buff, lenbuf, "long varchar(%d)", definesize);
		break;

	case  adLongVarWChar:
		sprintf_s( buff, lenbuf, "long nvarchar(%d)", definesize);
		break;

	case  adNumeric:
		sprintf_s( buff, lenbuf, "numeric(%d, %d)", definesize, field->Precision);
		break;

	case  adPropVariant:
		sprintf_s( buff, lenbuf, "prop variant(%d)", definesize);
		break;

	case  adSingle:
		sprintf_s( buff, lenbuf, "float(%d)", definesize);
		break;

	case  adSmallInt:
		sprintf_s( buff, lenbuf, "smallint(%d)", definesize);
		break;

	case  adTinyInt:
		sprintf_s( buff, lenbuf, "tinyint(%d)", definesize);
		break;

	case  adUnsignedBigInt:
		sprintf_s( buff, lenbuf, "unsigned bigint(%d)", definesize);
		break;

	case  adUnsignedInt:
		sprintf_s( buff, lenbuf, "unsigned(%d)", definesize);
		break;

	case  adUnsignedSmallInt:
		sprintf_s( buff, lenbuf, "smallint(%d)", definesize);
		break;

	case  adUnsignedTinyInt:
		sprintf_s( buff, lenbuf, "tinyint(%d)", definesize);
		break;

	case  adUserDefined:
		sprintf_s( buff, lenbuf, "userdefined(%d)", definesize);
		break;

	case  adVarBinary:
		sprintf_s( buff, lenbuf, "varbinary(%d)", definesize);
		break;

	case  adVarChar:
		sprintf_s( buff, lenbuf, "varchar(%d)", definesize);
		break;

	case  adVariant:
		sprintf_s( buff, lenbuf, "variant(%d)", definesize);
		break;

	case  adVarNumeric:
		sprintf_s( buff, lenbuf, "varnumeric(%d)", definesize);
		break;

	case  adVarWChar:
		sprintf_s( buff, lenbuf, "nvarchar(%d)", definesize);
		break;

	case  adWChar:
		sprintf_s( buff, lenbuf, "nchar(%d)", definesize);
		break;

	default:
		sprintf_s( buff, lenbuf, "unkown(%d)", definesize);
		break;
	}
	return buff;
}


/*
** Creates the list of fields names and pushes it on top of the stack.
*/
static void create_colnames (lua_State *L, cur_data *cur) {
	//PGresult *result = cur->ado_res;
	int i;
	int colscount = cur->ado_res->Fields->GetCount();
	lua_newtable (L);
	try{
		for (i = 0; i < colscount; i++) {
			//const char* fn = cur->ado_res->Fields->GetItem( _variant_t(i) )->Name;
			_variant_t vi( (long)i,  VT_I4 );
			
			FieldPtr fd = cur->ado_res->Fields->GetItem( vi );
			_bstr_t fdname = fd->GetName();
			lua_pushstring (L, fdname);
			lua_rawseti (L, -2, i);
			
		}
	}catch( _com_error e )
	{
		luasql_faildirect_com_error( L, e );
	}
}


/*
** Creates the list of fields types and pushes it on top of the stack.
*/
static void create_coltypes (lua_State *L, cur_data *cur) {
	int colscount = cur->ado_res->Fields->GetCount();
	lua_newtable (L);
	try{
		char buff[128] = {0};
		for (int i = 0; i < colscount; i++) {
			//const char* fn = cur->ado_res->Fields->GetItem( _variant_t(i) )->Name;
			_variant_t vi( (long)i,  VT_I4 );
			FieldPtr fd = cur->ado_res->Fields->GetItem( vi );
			getcolumntype( fd, buff, sizeof(buff) );
			lua_pushstring (L, buff);
			lua_rawseti (L, -2, i);
			
		}
	}catch( _com_error e )
	{
		luasql_faildirect_com_error( L, e );
	}

}


/*
** Pushes a column information table on top of the stack.
** If the table isn't built yet, call the creator function and stores
** a reference to it on the cursor structure.
*/
static void _pushtable (lua_State *L, cur_data *cur, size_t off, creator func) {
	int *ref = (int *)((char *)cur + off);
	if (*ref != LUA_NOREF)
		lua_rawgeti (L, LUA_REGISTRYINDEX, *ref);
	else {
		func (L, cur);
		/* Stores a reference to it on the cursor structure */
		lua_pushvalue (L, -1);
		*ref = luaL_ref (L, LUA_REGISTRYINDEX);
	}
}
#define pushtable(L,c,m,f) (_pushtable(L,c,offsetof(cur_data,m),f))


/*
** Return the list of field names.
*/
static int cur_getcolnames (lua_State *L) {
	pushtable (L, getcursor(L), colnames, create_colnames);
	return 1;
}


/*
** Return the list of field types.
*/
static int cur_getcoltypes (lua_State *L) {
	pushtable (L, getcursor(L), coltypes, create_coltypes);
	return 1;
}


/*
** Push the number of rows.
*/
static int cur_numrows (lua_State *L) {
	lua_pushnumber (L, getcursor(L)->numrows);
	return 1;
}



/*
** return next result  next.
*/
static int cur_next (lua_State *L) {
	//lua_pushnumber (L, getcursor(L)->numrows);
	cur_data* cur = getcursor(L);
	_variant_t affect;
	int rows = -1;
	try{
		_RecordsetPtr result = cur->ado_res->NextRecordset( &affect );
		if( affect.vt != VT_EMPTY )
		{
			rows = affect.intVal;
		}
		return create_cursor(L, rows, result.Detach() );
	}catch( _com_error ex )
	{
		luasql_faildirect_com_error( L, ex );
		return 0;
	}
}
/*
** Create a new Cursor object and push it on top of the stack.
*/
static int create_cursor (lua_State *L, int rows, struct _Recordset* result) {
	cur_data *cur = (cur_data *)lua_newuserdata(L, sizeof(cur_data));
	luasql_setmeta (L, LUASQL_CURSOR_ADODB);

	/* fill in structure */
	cur->closed = 0;
	cur->conn = LUA_NOREF;
	if( rows > 0 ){
		cur->numrows = rows;
	}
	try{
		if( result->RecordCount > 0 )
		{
			cur->numrows = result->RecordCount;
		} else {
			cur->numrows = -1;
		}
	}catch( _com_error e )
	{
		//cur->numrows = -1;
	}

	cur->numcols =  result->GetFields()->Count;//PQnfields(result);
	cur->colnames = LUA_NOREF;
	cur->coltypes = LUA_NOREF;
	cur->curr_tuple = 0;
	cur->ado_res = result;
	lua_pushvalue (L, 1);
	cur->conn = luaL_ref (L, LUA_REGISTRYINDEX);

	return 1;
}


static void sql_commit(conn_data *conn) {
	//PQclear(PQexec(conn->pg_conn, "COMMIT"));
	conn->ado_conn->CommitTrans();
}


static void sql_begin(conn_data *conn) {
	//PQclear(PQexec(conn->pg_conn, "BEGIN")); 
	conn->ado_conn->BeginTrans();
}


static void sql_rollback(conn_data *conn) {
	conn->ado_conn->RollbackTrans();
	//PQclear(PQexec(conn->pg_conn, "ROLLBACK"));
}

static int conn_begin(lua_State* L) {
	conn_data *conn = (conn_data *)luaL_checkudata (L, 1, LUASQL_CONNECTION_ADODB);
	luaL_argcheck (L, conn != NULL, 1, LUASQL_PREFIX"connection expected");
	if( conn->closed )
	{
		lua_pushboolean (L, 0);
		return 1;
	}
	sql_begin( conn );
	return 1;
}


/*
** Close a Connection object.
*/
static int conn_close (lua_State *L) {
	conn_data *conn = (conn_data *)luaL_checkudata (L, 1, LUASQL_CONNECTION_ADODB);
	luaL_argcheck (L, conn != NULL, 1, LUASQL_PREFIX"connection expected");
	
	if (conn->closed) {
		lua_pushboolean (L, 0);
		return 1;
	}

	/* Nullify structure fields. */
	conn->closed = 1;
	luaL_unref (L, LUA_REGISTRYINDEX, conn->env);
	try{
		conn->ado_conn->Close();
	}catch( _com_error e){}
	lua_pushboolean (L, 1);
	return 1;
}


/*
** Escapes a string for use within an SQL statement.
** Returns a string with the escaped string.
*/
static int conn_escape (lua_State *L) {
	return 0;
}


/*
** Execute an SQL statement.
** Return a Cursor object if the statement is a query, otherwise
** return the number of tuples affected by the statement.
*/
static int conn_execute (lua_State *L) {
	conn_data *conn = getconnection (L);
	const char *statement = luaL_checkstring (L, 2);
	int   option = -1;
	if( lua_isnumber(L, 3) ){
		option = luaL_checkint(L, 3);
	}
	try{
		_CommandPtr cmd;
		_RecordsetPtr res;
		_variant_t RecordsAffected;
		_variant_t vNull;
		cmd.CreateInstance( _uuidof(Command) );
		cmd->CommandText = statement;
		cmd->CommandType = adCmdText;
		cmd->ActiveConnection = _ConnectionPtr( conn->ado_conn );

		if( lua_istable(L, 4) ){
			//lua_gettable(L, 4);
			int t = lua_gettop(L);
			lua_pushnil(L);
			while( lua_next(L, t) )
			{
				if( lua_isstring(L, -1) )
				{
					int isStrParam = lua_isstring(L, -2);
					int textLength = -1;
					const char* value = NULL;
					const char* key = lua_tostring( L, -2);
					_variant_t vt;
					if( isStrParam ){
						value = lua_tostring(L, -1);
						if( value == NULL ){
							textLength = 0;
						}else{
							textLength = strlen(value);
						}
						vt = value;
					}else{
						vt = lua_tonumber(L, -1);
					}
					_ParameterPtr param = cmd->CreateParameter( key, isStrParam ? adVarChar : adNumeric, adParamInput, textLength, vt );
					cmd->Parameters->Append( param );
				}
				lua_pop(L,1);
			}
			//cmd->Prepared = VARIANT_TRUE;
			cmd->put_Prepared(VARIANT_TRUE);
		}

		
		//cmd->Prepared = VARIANT_TRUE;
		res = cmd->Execute( NULL, NULL, option);
		
		//conn->ado_conn->AddRef();
		//res = conn->ado_conn->Execute( _bstr_t(statement), &RecordsAffected, 0 );

		//conn->ado_conn->Release();
		return create_cursor (L, RecordsAffected.intVal, res.Detach());
		
	}catch( _com_error e )
	{
		return luasql_faildirect(L, _bstr_t(e.ErrorMessage()));
	}
}


/*
** Commit the current transaction.
*/
static int conn_commit (lua_State *L) {
	conn_data *conn = getconnection (L);
	sql_commit(conn);
	if (conn->auto_commit == 0) {
		sql_begin(conn); 
		lua_pushboolean (L, 1);
	} else
		lua_pushboolean (L, 0);
	return 1;
}


/*
** Rollback the current transaction.
*/
static int conn_rollback (lua_State *L) {
	conn_data *conn = getconnection (L);
	sql_rollback(conn);
	if (conn->auto_commit == 0) {
		sql_begin(conn); 
		lua_pushboolean (L, 1);
	} else
		lua_pushboolean (L, 0);
	return 1;
}


/*
** Set "auto commit" property of the connection.
** If 'true', then rollback current transaction.
** If 'false', then start a new transaction.
*/
static int conn_setautocommit (lua_State *L) {
	conn_data *conn = getconnection (L);
	if (lua_toboolean (L, 2)) {
		conn->auto_commit = 1;
		sql_rollback(conn); /* Undo active transaction. */
	}
	else {
		conn->auto_commit = 0;
		sql_begin(conn);
	}
	lua_pushboolean(L, 1);
	return 1;
}


/*
** Create a new Connection object and push it on top of the stack.
*/
static int create_connection (lua_State *L, int env, struct _Connection* ado_conn) {
	conn_data *conn = (conn_data *)lua_newuserdata(L, sizeof(conn_data));
	luasql_setmeta (L, LUASQL_CONNECTION_ADODB);

	/* fill in structure */
	conn->closed = 0;
	conn->env = LUA_NOREF;
	conn->auto_commit = 1;
	conn->ado_conn = ado_conn;
	
	lua_pushvalue (L, env);
	conn->env = luaL_ref (L, LUA_REGISTRYINDEX);
	return 1;
}


static void notice_processor (void *arg, const char *message) {
  (void)arg; (void)message;
  /* arg == NULL */
}


/*
** Connects to a data source.
** This driver provides two ways to connect to a data source:
** (1) giving the connection parameters as a set of pairs separated
**     by whitespaces in a string (first method parameter)
** (2) giving one string for each connection parameter, said
**     datasource, username, password, host and port.
*/
static int env_connect (lua_State *L) {
	const char *sourcename = luaL_checkstring(L, 2);
	_ConnectionPtr conn; 
	try{
		conn.CreateInstance(_uuidof(Connection));
		getenvironment (L);	/* validate environment */
		if ((lua_gettop (L) == 2) && (strchr (sourcename, '=') != NULL)){
			//conn = PQconnectdb (sourcename);
			conn->Open( sourcename, "", "", 0 );
		}
		else {
			const char *username = luaL_optstring(L, 3, NULL);
			const char *password = luaL_optstring(L, 4, NULL);
			const char *pghost = luaL_optstring(L, 5, NULL);
			const char *pgport = luaL_optstring(L, 6, NULL);


		}

	}catch(_com_error& e)
	{
		
		return luasql_faildirect(L,  _bstr_t(e.ErrorMessage()) );
	}
	//PQsetNoticeProcessor(conn, notice_processor, NULL);
	return create_connection(L, 1, conn.Detach());
}


/*
** Close environment object.
*/
static int env_close (lua_State *L) {
	env_data *env = (env_data *)luaL_checkudata (L, 1, LUASQL_ENVIRONMENT_ADODB);
	luaL_argcheck (L, env != NULL, 1, LUASQL_PREFIX"environment expected");
	if (env->closed) {
		lua_pushboolean (L, 0);
		return 1;
	}

	env->closed = 1;
	lua_pushboolean (L, 1);
	return 1;
}



/*
** Create metatables for each class of object.
*/
static void create_metatables (lua_State *L) {
    struct luaL_reg environment_methods[] = {
        {"close", env_close},
        {"connect", env_connect},
		{NULL, NULL},
	};
    struct luaL_reg connection_methods[] = {
        {"close", conn_close},
        {"escape", conn_escape},
        {"execute", conn_execute},
        {"commit", conn_commit},
        {"rollback", conn_rollback},
        {"setautocommit", conn_setautocommit},
		{"begin", conn_begin},
		{NULL, NULL},
    };
    struct luaL_reg cursor_methods[] = {
        {"close", cur_close},
        {"getcolnames", cur_getcolnames},
        {"getcoltypes", cur_getcoltypes},
        {"fetch", cur_fetch},
		{"numrows", cur_numrows},
		{"next", cur_next},
		{NULL, NULL},
    };
	luasql_createmeta (L, LUASQL_ENVIRONMENT_ADODB, environment_methods);
	luasql_createmeta (L, LUASQL_CONNECTION_ADODB, connection_methods);
	luasql_createmeta (L, LUASQL_CURSOR_ADODB, cursor_methods);
	lua_pop (L, 3);
}

/*
** Creates an Environment and returns it.
*/
static int create_environment (lua_State *L) {
	env_data *env = (env_data *)lua_newuserdata(L, sizeof(env_data));
	luasql_setmeta (L, LUASQL_ENVIRONMENT_ADODB);

	/* fill in structure */
	env->closed = 0;
	return 1;
}


/*
** Creates the metatables for the objects and registers the
** driver open method.
*/
LUASQL_API int luaopen_luasql_adodb (lua_State *L) {
    struct luaL_reg driver[] = {
		{"adodb", create_environment},
		{NULL, NULL},
	};
	//CoInitialize( NULL );
	create_metatables (L);
	luaL_openlib (L, LUASQL_TABLENAME, driver, 0);
	luasql_set_info (L);
	return 1;
}
